using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using Boo.Lang;
using Castle.Core.Logging;
using Mite.Configuration;

namespace Mite
{
	public abstract class Adapter : IAdapter
	{
		/// <summary>
		/// Dependency injection property.
		/// </summary>
		/// <remarks>
		/// If this object is instantiated with windsor and a logging facility is available,
		/// it will be automatically injected.
		/// </remarks>
		public ILogger Logger
		{
			get { return _logger; }
			set
			{
				if (value == null)
					_logger = NullLogger.Instance;
				else
					_logger = value;
			}
		}

		/// <summary>
		/// Query factory to generate the sql statements for the adapter.
		/// </summary>
		public ISqlQueryFactory QueryFactory { get; set; }

		public abstract IDbConnection CreateConnection(string host, string database, string username, string password,
													   Hash options);

		public abstract IEnumerable<Database> GetDatabases(IDbConnection connection);

		public IDbCommand CreateTable(IDbConnection connection, Table table)
		{
			string sql = QueryFactory.CreateTable(table);

			IDbCommand cmd = connection.CreateCommand();
			cmd.CommandText = sql;

			return cmd;
		}

		public IDbCommand DropTable(IDbConnection connection, Table table)
		{
			string sql = QueryFactory.DropTable(table);

			IDbCommand cmd = connection.CreateCommand();
			cmd.CommandText = sql;

			return cmd;
		}

		/// <summary>
		/// Rename <paramref name="table"/>.
		/// </summary>
		/// <param name="table">Table to rename.</param>
		/// <param name="newName">New name of the table.</param>
		/// <param name="connection">Connection with which to associate the returned command.</param>
		/// <returns>Command to rename the table.</returns>
		public IDbCommand RenameTable(IDbConnection connection, Table table, string newName)
		{
			string sql = QueryFactory.RenameTable(table, newName);

			IDbCommand cmd = connection.CreateCommand();
			cmd.CommandText = sql;

			return cmd;
		}

		/// <summary>
		/// Rename <paramref name="column"/>.
		/// </summary>
		/// <param name="column">Column to rename.</param>
		/// <param name="newName">New name of <paramref name="column"/>.</param>
		/// <param name="connection">Connection with which to associate the returned command.</param>
		/// <returns>Command to rename column.</returns>
		public IDbCommand RenameColumn(IDbConnection connection, Column column, string newName)
		{
			string sql = QueryFactory.RenameColumn(column, newName);

			IDbCommand cmd = connection.CreateCommand();
			cmd.CommandText = sql;

			return cmd;
		}

		public IDbCommand AlterColumn(IDbConnection connection, Column column, Column newColumn)
		{
			// TODO: Integrate with AlterationValidation

			string sql = QueryFactory.AlterColumn(column, newColumn);

			IDbCommand cmd = connection.CreateCommand();
			cmd.CommandText = sql;

			return cmd;
		}

		public IDbCommand AddColumnToTable(IDbConnection connection, Table table, Column column)
		{
			string sql = QueryFactory.AddColumnToTable(table, column);

			IDbCommand cmd = connection.CreateCommand();
			cmd.CommandText = sql;

			return cmd;
		}

		public abstract IDbCommand SetSchemaVersion(IDbConnection connection, int i);

		public AlterationValidation AlterationValidator { get; set; }

		/// <summary>
		/// Create the migration schema table that contains meta data about
		/// migration version in <paramref name="database"/>.
		/// </summary>
		public virtual IDbCommand[] CreateSchemaTable(IDbConnection connection, Database database)
		{
			var schemaTable = new Table(MIGRATION_SCHEMA_TABLE_NAME,
										new Column("Id", DbType.Int32, ColumnAttributes.Identity | ColumnAttributes.PrimaryKey),
										new Column("Version", DbType.Int32, ColumnAttributes.NotNull),
										new Column("ExecutedAt", DbType.DateTime, ColumnAttributes.NotNull));


			var cmds = new List<IDbCommand>();
			cmds.Add(CreateTable(connection, schemaTable));

			IDbCommand insertCommand = Insert(connection, MIGRATION_SCHEMA_TABLE_NAME, 0, DateTime.Now);

			cmds.Add(insertCommand);
			return cmds.ToArray();
		}

		// TODO: Make this method virtual and have it us QueryFactory to retrieve a template for its query.
		public abstract Database GetDatabase(IDbConnection connection, string name);

		public virtual int GetSchemaVersion(IDbConnection conn, Database database)
		{
			Table table = GetSchemaTable(database);
			if (table == null)
				return 0;
			Column column = table.Columns.Single(c => c.Name.Equals("Version"));
			Query schemaQuery = Query.For(table, conn).Select(column);
			IDictionary<Column, ArrayList> results = schemaQuery.ExecuteReader();
			results[column].Reverse();
			var version = (int)results[column][0];
			return version;
		}

		/// <summary>
		/// Creates an index.
		/// </summary>
		/// <remarks>
		/// It is the responsibilty of the caller to dispose of the command returned by this method.
		/// </remarks>
		/// <param name="index">The index to create.</param>
		/// <param name="connection">Connection with which to associate the <see cref="IDbCommand"/>.</param>
		/// <returns>Command to create the index.</returns>
		public virtual IDbCommand CreateIndex(Index index, IDbConnection connection)
		{
			Guard.ArgumentNotNull("index", index);
			Guard.ArgumentNotNull("connection", connection);

			string sql = QueryFactory.CreateIndex(index);

			IDbCommand cmd = connection.CreateCommand();
			cmd.CommandText = sql;

			return cmd;
		}

		/// <summary>
		/// Drops an index.
		/// </summary>
		/// <remarks>
		/// It is the responsibilty of the caller to dispose of the command returned by this method.
		/// </remarks>
		/// <param name="index">The index to drop.</param>
		/// <param name="transaction">
		/// Transaction with which to associate the <see cref="IDbCommand"/>.  
		/// The transaction must be associated with a connection.
		/// </param>
		/// <returns>Command to drop the index.</returns>
		public virtual IDbCommand DropIndex(Index index, IDbTransaction transaction)
		{
			Guard.ArgumentNotNull("transaction", transaction);

			IDbCommand cmd = DropIndex(index, transaction.Connection);
			cmd.Transaction = transaction;

			return cmd;
		}

		/// <summary>
		/// Drops an index.
		/// It is the responsibility of the caller to dispose of the <see cref="IDbCommand"/> returned by this method.
		/// </summary>
		/// <param name="index">The index to drop.</param>
		/// <param name="connection">Connection with which to associate the <see cref="IDbCommand"/>.</param>
		/// <returns>Command to drop the index.</returns>
		public virtual IDbCommand DropIndex(Index index, IDbConnection connection)
		{
			Guard.ArgumentNotNull("index", index);
			Guard.ArgumentNotNull("connection", connection);

			string sql = QueryFactory.DropIndex(index);

			IDbCommand cmd = connection.CreateCommand();
			cmd.CommandText = sql;

			return cmd;
		}

		/// <summary>
		/// Generate command to drop <paramref name="columns"/>.
		/// </summary>
		/// <param name="connection">Connection with which to associate the commands.</param>
		/// <param name="table">Table to which the columns belong.</param>
		/// <param name="columns">Columns to drop.</param>
		/// <returns>A command to drop <paramref name="columns"/>.</returns>
		public IDbCommand DropColumns(IDbConnection connection, string table, params string[] columns)
		{
			Guard.ArgumentNotNull("connection", connection);
			Guard.SequenceNotNullOrEmpty("columns", columns);

			IDbCommand cmd = connection.CreateCommand();
			cmd.CommandText = QueryFactory.DropColumns(table, columns);

			return cmd;
		}

		public IDbCommand AddForeignKey(IDbConnection connection, string table, string referenceTable, string name, params KeyValuePair<string, string>[] referenceMap)
		{
			Guard.ArgumentNotNull("connection", connection);
			Guard.StringArgumentNotNullOrEmpty("name", name);

			IDbCommand cmd = connection.CreateCommand();
			cmd.CommandText = QueryFactory.AddForeignKey(table, referenceTable, name, referenceMap);

			return cmd;
		}

		/// <summary>
		/// Remove foreign key constraint.
		/// </summary>
		/// <returns>Command to remove <paramref name="name"/>.</returns>
		public IDbCommand DropForeignKey(IDbConnection connection, string table, string name)
		{
			Guard.ArgumentNotNull("connection", connection);
			Guard.StringArgumentNotNullOrEmpty("table", table);
			Guard.StringArgumentNotNullOrEmpty("name", name);

			IDbCommand cmd = connection.CreateCommand();
			cmd.CommandText = QueryFactory.DropConstraint(table, name);

			return cmd;
		}

		protected abstract DbType ParseDbType(string dataType);

		public static Adapter Create(MiteEnvironment environment)
		{
			Guard.ArgumentNotNull("environment", environment);
			if (!environment.Adapter.IsSubclassOf(typeof(Adapter)))
				throw new InvalidOperationException("environment.Adapter is not a subclass of Mite.Data.Adapter");

			return (Adapter)Activator.CreateInstance(environment.Adapter);
		}

		protected abstract IDbCommand Insert(IDbConnection connection, string tableName, params object[] values);

		protected static Table GetSchemaTable(Database database)
		{
			return database.Tables.SingleOrDefault(tbl => tbl.Name.Equals(MIGRATION_SCHEMA_TABLE_NAME));
		}

		public static string MIGRATION_SCHEMA_TABLE_NAME = "migration_schema";
		protected ILogger _logger = NullLogger.Instance;
	}
}